package com.laijiale.component;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.laijiale.util.JwtUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

/**
 * @author laijiale
 * @date 2025年01月08日 16:10
 */
@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 白名单请求直接放行
        if (isWhite(request)) {
            return chain.filter(exchange);
        }

        // 获取token令牌
        String token = getToken(request);

        // Token令牌不存在
        if (StringUtils.isBlank(token)) {
            return buildResponseData(response, 1001, "Token过期或失效", "");
        }

        // 解析Token令牌
        Map<String, Object> verifyResult;
        try {
            verifyResult = JwtUtil.verify(token);
        } catch (RuntimeException e) {
            return buildResponseData(response, 1001, "Token过期或失效", "");
        }

        // 若Token令牌即将过期，则返回过期提示以及一个新的Token令牌
        if ((boolean) verifyResult.get("expiringSoon")) {
            return buildResponseData(response, 1002, "Token即将过期", verifyResult.get("newToken"));
        }

        // 放行请求
        return chain.filter(exchange);
    }

    /**
     * 配置过滤器的优先级
     *
     * @return 值越小，优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 构建响应数据
     *
     * @param response 响应对象
     * @param code     响应代码
     * @param message  响应代码描述
     * @param data     响应数据
     * @return 响应数据的Mono对象
     */
    private Mono<Void> buildResponseData(ServerHttpResponse response,
                                         int code, String message, Object data) {
        // 设置响应类型
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        // 设置响应数据（Map）
        Map<String, Object> resultMap = Map.of("code", code, "message", message, "data", data);
        // 设置响应数据（Map -> JSON String）
        String resultStr = JSONUtil.toJsonStr(resultMap);
        // 设置响应数据（JSON String -> byte[]）
        byte[] result = resultStr.getBytes(StandardCharsets.UTF_8);
        // 响应
        return response.writeWith(Flux.just(response.bufferFactory().wrap(result)));
    }

    /**
     * 判断请求是否在白名单中
     *
     * @param request 请求对象
     * @return true在白名单中，false不在白名单中
     */
    private boolean isWhite(ServerHttpRequest request) {
        boolean result = false;
        final List<String> whites = List.of(
                "/api/v1/banner/top",
                "/api/v1/article/top",
                "/api/v1/notice/top",
                "/api/v1/course/searchByTitle",
                "/api/v1/user/loginByAccount",
                "/api/v1/user/loginByPhone",
                "/api/v1/user/register");

        String url = request.getURI().toString();
        for (String white : whites) {
            if (url.contains(white)) {
                result = true;
                break;
            }
        }
        return result;
    }

    /**
     * 依次尝试从请求头和请求参数中获取Token令牌
     *
     * @param request 请求对象
     * @return 获取成功返回token令牌，获取失败返回null
     */
    private String getToken(ServerHttpRequest request) {
        // 尝试从请求头中获取Token令牌
        String token = request.getHeaders().getFirst("token");
        if (StringUtils.isBlank(token)) {
            // 尝试从查询串中获取Token令牌
            token = request.getQueryParams().getFirst("token");
        }
        return token;
    }
}